core, blockstm, state: add BlockSTM v2 parallel transaction execution#2210
core, blockstm, state: add BlockSTM v2 parallel transaction execution#2210
Conversation
Introduces BlockSTM v2 — a from-scratch redesign of Bor's parallel
transaction execution engine. V2 speculatively executes block
transactions concurrently, validates each tx's reads against a
multi-version store, and re-executes any whose reads turned stale.
On the 241-block mainnet witness benchmark V2/4w delivers ~1.6×
throughput over serial (570 mgas/s vs 350 mgas/s, AMD Ryzen 7 5800H,
all-in-memory).
V2 runs three coordinated goroutine groups around a per-tx PDB:
V2StateProcessor.Process (core/parallel_state_processor.go)
│
ExecuteV2BlockSTM (core/blockstm/v2_executor.go)
│
┌─────┼──────────────┬──────────────┐
│ │ │ │
Workers (N) Validator (1) Settlement (1)
ParallelStateDB StoreReads finalDB
BalReads IntermediateRoot
Backed by:
SafeBase Thread-safe base reads (sync.Map caches over a
bounded pool of StateDB.Copy() with concurrent-
reads mode on the trieReader)
MVStore Sharded multi-version per-key store with a
lock-free bloom filter for cold-key reads
MVBalanceStore Sharded commutative balance delta store
(per-tx Add/Sub; reads sum prior entries)
1. Task building. Block transactions become V2Tasks. Same-sender
chains get pre-computed nonces (SenderNonces) so nonce reads
on a chain are skipped during validation.
2. Parallel execution. N worker goroutines pull tasks from a
buffered dispatcher (window numWorkers * InFlightTaskMultiplier).
Each tx runs in its own ParallelStateDB; reads come from
SafeBase + MVStore + MVBalanceStore and are recorded in
StoreReads / BalReads. Writes accumulate locally (DeferMVWrites)
and flush to MVStore at end-of-tx so concurrent readers only
ever see FINAL values — never mid-tx reentrancy-guard writes.
3. Sequential validation. A single goroutine validates txs in
tx-index order. Each recorded read is re-checked against MVStore;
match by writer/incarnation OR by value-equal fallback (handles
idempotent writes such as reentrancy-guard SSTOREs that flip
back). Mismatch → MarkEstimate the failed tx's writes and
dispatch a re-execution goroutine. Per-key pipelining: readers
that hit an ESTIMATE entry under Incarnation > 0 block on
WaitForFinal until the upstream writer is finalized.
4. Pipelined settlement. As txs finalize, a settlement goroutine
drains chSettle in tx-index order and applies each tx's writes
to finalDB (the real, single-threaded *state.StateDB) through a
*Direct setter family that bypasses the journal, then asks
finalDB for the IntermediateRoot.
V2 is gated on a layered test surface. From cheapest to most
expensive, and what each layer is meant to catch:
1. Compile-time conformance + drift detection
The PDB shadows StateDB's interface and behaviour, so any
upstream go-ethereum merge that adds or changes a StateDB
method would silently bypass V2. A handful of `go test`-time
checks fail CI before any logic runs:
- core/vm/statedb_impl_test.go (PDB satisfies vm.StateDB
via a static assertion)
- TestPDBMethodParity (every StateDB method has
a PDB mapping or is in
pdbExemptMethods)
- TestV2DependencyCompileCheck (every StateDB method V2
settle calls remains present)
- TestV2JournalEntryCoverage (every journal entry kind has
a parallelJournalEntry mapping)
- TestV2TracingHookParity (every tracing.Hooks field is
classified as fired-or-skipped)
- TestV2ForkParity (every params.ChainConfig.IsX
fork rule is classified V1/V2)
2. Per-method unit tests (~210 tests across ~25 files)
Cover individual PDB getters/setters, MVStore / MVBalanceStore
primitives, V2 executor channel mesh, and SettleTo helpers.
Highlights:
- core/state/parallel_statedb_test.go (76 tests; PDB
behaviour + the
Tier-1 mutation
kill suite — see
layer 5 below)
- core/state/parallel_statedb_coverage_test.go (42 tests;
branch coverage)
- core/state/parallel_statedb_getter_table_test.go (every PDB
getter records
its read with
the right WriterIdx
across Committed /
ESTIMATE / NoEntry /
AtTxZero)
- core/state/safe_base_test.go (sync.Map cache +
pool semantics)
- core/blockstm/mvstore_test.go,
core/blockstm/mvbalance_store_test.go (versioned store
primitives)
- core/blockstm/v2_executor_wait_test.go (waitForTx /
waitForFinal +
cancellation)
3. Direct-setter parity tests
The *Direct setter family bypasses StateDB's journal at settle
time. core/state/v2_direct_setter_parity_test.go (7 tests) pins
that SetXDirect produces a byte-identical state root to journaled
SetX + Finalise. Catches divergence the moment a future change
to either path breaks the parity.
4. Differential tests vs serial StateDB
Hand-written + table-driven scenarios that exercise the PDB
against a parallel-mirror serial StateDB and assert byte-identical
output. Catches behaviour drift the parity-table tests can't
express:
- core/state/v2_differential_test.go (PDB-only diff)
- core/state/v2_executor_differential_test.go (synthetic-env
executor diff)
- core/v1_differential_test.go (V1 vs serial
parity for the
legacy in-tree path)
5. Mutation testing (Tier-1 kill tests)
diffguard runs mutation testing against V2's critical paths.
Every survivor flagged by a sample run has a corresponding
targeted test inline in core/state/parallel_statedb_test.go
under the "Tier-1 mutation kill tests" divider — boundary,
negation, and return-value mutations on storeReadMatches,
journal revert, settleTo helpers, applyFeeData, Reset, etc.
Tier-1 logic kill-rate ≥ 99% on the latest run.
6. Fuzz targets
Randomized inputs against either a serial mirror or a hand-built
reference:
- core/state/v2_fuzz_test.go (random PDB op
sequences vs StateDB)
- core/state/v2_executor_fuzz_test.go (executor-level fuzz
on synthetic env)
- core/v2_serial_parity_fuzz_test.go (FuzzV2ExecutorVsSerial:
random tx batches
through ExecuteV2BlockSTM
vs an ApplyMessage loop)
The race-detected fuzz under `-race` caught the shared-trie-reader
race that the non-race fuzz missed; worth keeping on the nightly.
7. End-to-end consistency + benchmark on real mainnet blocks
core/mainnet_witness_benchmark_test.go bundles 241 real Polygon
mainnet blocks (under core/blockstm/testdata/) with their pre-
block witnesses. Two harnesses share the corpus:
- TestV2BlockSTMAllBlocks (gated on BOR_BLOCKSTM_TEST=1)
replays each block through both serial and V2 and asserts
byte-identical state roots and receipt roots.
- BenchmarkV2AllBlocks runs serial + V2 across worker counts
(4 / 8 / 16) and witness-on/off variants on the same corpus.
Backs the throughput numbers referenced at the top of this
commit.
8. Runtime invariants under -tags=invariants
Build-tag-gated runtime assertions inside the executor and the
PDB. Off in production builds (zero-cost), on in CI:
- assertSettleOrder (validation walk's induction)
- assertReexecVisitedExactlyOnce (drain loop doesn't lose a tx)
- assertSettleNotPanicked (panicked PDBs never settle)
A tiny set of "panic if invariant breaks" tests under
//go:build invariants verifies the assertions actually fire on
crafted violations (core/blockstm/v2_executor_invariants_panic_test.go,
core/state/parallel_statedb_invariants_panic_test.go).
9. Race detector
All of layers 2-8 are runnable under `go test -race`. CI runs
the full state + blockstm packages in race mode; the
TestV2BlockSTMAllBlocks gated test is also race-clean on the
241-block corpus.
10. Production soak — >1 million Polygon mainnet blocks
Beyond the unit / parity / fuzz layers above, this branch has
been used to sync more than 1,000,000 mainnet blocks end-to-end
on a real node with V2 as the primary processor (with serial
disabled). Zero state-root divergences, zero panics
requiring fallback, no consensus-affecting issues observed.
This is the most stringent layer: real on-chain workload,
real database backend, real prefetcher contention.
- intermediateRootTimer metric (chain/intermediateroot) — measures
the post-execution trie computation in block_validator.go.
The code surface is ~5.1k lines across 39 production .go files,
plus ~11.7k lines across 37 test files. The remaining 484 file
entries in the diff are block + witness fixtures under
core/blockstm/testdata used by TestV2BlockSTMAllBlocks and the
benchmark harness — read-only data, no review needed.
Shapes of change a reviewer should expect:
- New per-tx state. ParallelStateDB shadows *state.StateDB but
reads from SafeBase + MVStore + MVBalanceStore and tracks reads
for validation. Implements vm.StateDB. Has its own journal
layer (parallelJournalEntry) parallel to StateDB's journal.go.
- New concurrent stores. MVStore (sharded multi-version per-key
store with bloom filter) and MVBalanceStore (sharded
commutative balance deltas) — both new, both load-bearing.
- New executor. ExecuteV2BlockSTM owns the worker pool +
in-order validator + pipelined settle goroutine and the
chSettle / completionCh / execDone channel mesh between them.
- Concurrent-safe base reads. SafeBase is a thread-safe wrapper
around a *state.StateDB with sync.Map caches + a bounded pool
of db.Copy() instances; the pool copies share the underlying
reader, so the V2 entry point flips trieReader into its
concurrent-reads mode (sync.Map node-resolve cache instead of
in-place mutation) — this required surgery in state/database.go,
state/reader.go, state/trie_prefetcher.go, trie/trie.go,
trie/secure_trie.go, triedb/pathdb/reader.go, and
triedb/pathdb/biased_fastcache.go.
- *Direct setter family on StateDB. Bypass the journal at
settle time so V2 can replay per-tx PDB writes onto finalDB
deterministically. Pinned byte-equal to journaled SetX +
Finalise by TestDirectSetterParity_*.
- Production fallback. BlockChain wires V2 as the primary
processor and falls back to serial on panics, ApplyMessage
consensus errors, ctx cancellation, and witness requests.
Tier 1 — load-bearing executor + per-tx state:
core/blockstm/v2_executor.go (+631 new)
core/parallel_state_processor.go (+925 V2StateProcessor,
settle-fn closure, env)
core/state/parallel_statedb.go (+1147 new)
core/state/parallel_statedb_validate.go (+223 new)
core/state/parallel_statedb_settle.go (+195 new)
core/state/parallel_statedb_journal.go (+127 new)
core/state/safe_base.go (+207 new)
Tier 2 — concurrent stores:
core/blockstm/mvstore.go (+186 new)
core/blockstm/mvbalance_store.go (+175 new)
Tier 3 — modified upstream files (highest merge-conflict risk):
core/state/statedb.go (Direct setters,
skipTimers, concurrent
reads enabler)
core/state/state_object.go (concurrent-safe getters)
core/state/database.go (concurrent reader)
core/state/reader.go (cache attribution)
core/state/trie_prefetcher.go (concurrent prefetch)
trie/trie.go, trie/secure_trie.go (concurrent-reads mode)
triedb/pathdb/reader.go (sync.Map node-resolve
cache for concurrent
reads; small lock changes)
triedb/pathdb/biased_fastcache.go (lock semantics)
core/vm/evm.go, jumpdests.go, (jumpdest cache sharing,
instructions.go, interface.go, precompile-cache,
interpreter.go StateDB iface adds)
core/blockchain.go (V2 wiring + fallback)
core/state_transition.go (interrupt plumbing)
Tier 4 — drift-detection tests (read these to understand the
parity contract V2 must hold against StateDB):
core/state/v2_method_parity_test.go (every StateDB method
has a PDB mapping)
core/state/v2_journal_entry_coverage_test.go
(every journal kind has
a parallel mapping)
core/state/v2_direct_setter_parity_test.go
(SetXDirect ↔ journaled)
core/state/parallel_statedb_getter_table_test.go
(every PDB getter
records its read)
core/parallel_state_processor_hooks_parity_test.go
(tracing.Hooks fire-or-
skip classification)
core/parallel_state_processor_fork_parity_test.go
(params.IsX classification)
core/v2_serial_parity_fuzz_test.go (real-tx executor fuzz
vs serial)
core/mainnet_witness_benchmark_test.go (gated 241-block end-
to-end consistency +
benchmark harness)
See docs/blockstm-v2.md for full architectural detail, the list of
correctness bug classes V2 prevents, and ongoing-improvement notes.
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
|
@claude review |
|
Claude finished @marcello33's task in 4m 15s —— View job Code Review: BlockSTM v2 Parallel Transaction Execution
This is a thorough, well-engineered redesign of Bor's parallel execution engine. The architecture — workers + single-goroutine validator + pipelined settlement — is sound and avoids most classic concurrency bugs by design. The test surface (10 layers including mutation testing, differential fuzz, and 241-block mainnet corpus) is exceptional for a change of this complexity. Below are the findings, organized by severity. Potential Issues1.
|
Cosmetic / micro-perf fixes from the claude[bot] review of #2210. No behaviour change. * state/statedb.go SubBalanceDirect — add a comment noting the uint256.Sub wrap matches the journaled SubBalance path (statedb.go:922) and that TestDirectSetterParity_SubBalance pins byte-equality between the two. A defensive panic was suggested but would diverge from the journaled path and break the parity test, so we keep the documentation-only. * state_transition.go SenderInitBalance — drop the inline IIFE nil check; input1 is GetBalance(...) which returns a value type, never nil. Straight input1.ToBig() matches the idiom used elsewhere in the function. * vm/evm.go runEcrecoverWithCache — drop the redundant RightPadBytes(input, 128) allocation. The [128]byte key is zero-initialised, so copy(key[:], input) achieves the same result without the extra heap allocation. Caller already guarantees len(input) <= 128. * vm/instructions.go opKeccak256 — replace size.SetBytes(cached. (common.Hash).Bytes()) with size.SetBytes32(h[:]) to skip the per-cache-hit Bytes() allocation on the SHA3 fast path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 241-block witness fixtures under core/blockstm/testdata/ are managed via Git LFS (~1.6 GB total). On a fresh clone or a CI runner that hasn't run `git lfs pull`, the .block / .witness.gz files are LFS pointer text stubs rather than the real data, and gzip.NewReader fails with "gzip: invalid header" — exactly what the unit-tests CI workflow has been hitting. Detect LFS pointers in readFileMaybeGz via the canonical "version https://git-lfs.github.com/spec/" prefix and surface a sentinel errLFSPointer error. loadEmbeddedBlocks and loadBlocksFromDir then call t.Skipf instead of t.Fatalf when the fixtures aren't materialized — the harness skips cleanly with a helpful message ("run `git lfs pull` to materialize testdata") instead of producing confusing gzip errors. This is the same prerequisite called out in docs/blockstm-v2.md → "Test data (Git LFS)". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lint workflow on #2210 flagged 15+ issues across V2 files. Fix each so `make lint` is clean. No behaviour change in production paths. * goimports — formatting on ~12 files (idiomatic import grouping + blank-line alignment that the in-tree gofmt missed). * unused — drop dead code: - executeWithParallelStateDBV2 + ValidatingParallelStateDB.checkBalance in core/mainnet_witness_benchmark_test.go (debug shims, never called). - timedMockV2State.execDelay + timedMockV2Env.fails in core/blockstm/v2_executor_test.go (vestigial fields). - ParallelStateDB.priorDestructed convenience wrapper (callers use priorDestructedAt). - opSubRefund / opWarmAddress diff ops in core/state/v2_differential_test.go (no scenario references them). * copyloopvar — drop the redundant `x := x` loop-variable copies across 8 test files (Go 1.22+ no longer needs them). * unconvert — drop the `time.Duration(result.Phase1)` cast (Phase1 is already time.Duration) and the `JumpDestCache(newMapJumpDests())` cast (already satisfies the interface). * durationcheck — fix `timeAfter(seconds time.Duration)` in core/blockstm/mvbalance_store_test.go: callers passed an int and the multiplication `seconds * time.Second` is a duration*duration bug. Make the parameter `int` and cast inside. * copylocks — `*statedb = *backupStateDB` in V1's maybeRerunWithoutFeeDelay copies a struct holding atomic.Int64. This is single-threaded V1 rerun-from-snapshot; tag with `//nolint:govet` and a comment. * whitespace — drop a leading blank line in v2Env.Execute that golangci-lint flagged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues flagged in the inline PR review of #2210. * core/blockchain.go: V2-failure fallback recovery was broken. cancel() ran BEFORE the `if result.parallel && result.err != nil` block, so when V2 finished first with an error (panic, ApplyMessage consensus error) the still-running serial processor was interrupted at its next tx boundary and the fallback `result = <-resultChan` received context.Canceled instead of a usable serial result. Documented as the recovery contract in the PR description. Move cancel() and followupInterrupt.Store(true) to AFTER the fallback block. The fallback (when V2 errors with V1 also running) gets a real serial result. Once we have the result we plan to return, cancel the loser so it stops at its next tx boundary before commit advances the pathdb layer (the original intent). See review comment r3187036031. * core/blockchain.go: drop unused AblationSkip* fields from BlockChain. Four exported boolean fields (AblationSkipFlush / AblationSkipSettle / AblationSkipFinalise / AblationSkipMVRead) were declared but never read or written anywhere — repo-wide grep confirms zero references outside the declaration site. The intended bridge from these BlockChain-level toggles to the per- block MVHashMap.Skip* fields (which ARE wired) was never threaded through, so flipping the BlockChain field was a silent no-op. Exported fields enter the API surface, so keeping them locks us into either a SemVer-breaking removal or maintainer confusion; drop them now and re-introduce as wired knobs in a separate change if/when the ablation experiments need a runtime entry point. See review comment r3187036037. * core/blockstm/mvhashmap.go: bloom h2 dimension was constant zero for the hottest key class. h2 read bytes [20:24] of Key, which are populated only for state keys; NewAddressKey leaves [20:52] zero and NewSubpathKey leaves [20:51] zero. h3 also half-degraded for those classes ([28:32] zero). Result: address-only and subpath reads collapsed the bloom from 3-of-3 to ~2-of-3, which doubles the false-positive rate at typical block sizes (~0.07% → ~0.35% at 1k unique keys). No correctness impact, just hot-path selectivity. Re-derive h2 from address bytes [16:20] (always populated) and fold the subpath/type bytes [52][53] into h3 so all three hashes draw from non-constant ranges for every key class. Updated the comment to reflect the new layout. See review comment r3187036040. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #2210 +/- ##
===========================================
+ Coverage 52.29% 52.74% +0.44%
===========================================
Files 884 894 +10
Lines 155571 158204 +2633
===========================================
+ Hits 81355 83441 +2086
- Misses 68989 69459 +470
- Partials 5227 5304 +77
... and 46 files with indirect coverage changes
🚀 New features to boost your workflow:
|
Three more issues flagged in the inline PR review of #2210. * core/parallel_state_processor.go: V2 was silently dropping the stateless-witness pointer. ProcessBlock wires the witness via parallelStatedb.StartPrefetcher("chain", witness, nil), but inside V2.Process the prefetcher is restarted with a hard-coded nil for the witness slot — and StateDB.StartPrefetcher unconditionally overwrites s.witness, so every s.witness != nil-gated collection point (CollectStateWitness, CollectCodeWitness, settle-phase trie walks) became a no-op for the rest of execution. On StatelessSelfValidation and single-block makeWitness paths the produced witness landed empty with no error. Fix: stash finalDB.Witness() before StopPrefetcher and pass it through to the v2-settle prefetcher restart, so the wired pointer survives the swap. See review comment r3191282978. * core/state/parallel_statedb.go: SelfDestruct skipped recordWrite for the SuicidePath key. FlushToMVStore writes (SuicidePath_addr, txIdx, inc, true) for every entry in s.destructed, but the key was never appended to s.WriteKeys, so MVStore.MarkEstimate / CleanupEstimate could not reach it on re-execution. Cross-incarnation invalidation was broken: a stale SuicidePath entry from incarnation N survived into N+1's view, and a downstream tx that observed it via priorDestructedAt could pass validation against state that no longer exists — a state-root divergence path. Every other MVStore-targeting writer (SetNonce, SetCode, SetState, CreateAccount) already calls recordWrite for the same reason; only the destruct path was missed. Fix: call s.recordWrite(NewSubpathKey(addr, SuicidePath)) inside the !s.destructed[addr] guard, matching the journal-entry guard so repeated SelfDestruct in the same tx doesn't append a duplicate. Pinned by TestPDB_SelfDestruct_RecordsSuicidePathWrite. See review comment r3191282996. * core/blockchain.go: V2 reader cache hit/miss stats were silently dropped. setupBlockReaders called ReadersWithCacheStatsTriple to create three independent ReaderWithStats wrappers (prefetch / process / parallel) and wired the parallel one into parallelStatedb, but reportReaderStats only consumed prefetch and process. V2's reads accumulated in the parallel wrapper's atomic counters and were discarded each block — and since V2 is the primary processor in production, the chain/state/account / storage/cache/{hit,miss} meters were essentially empty on the hot path. Fix: thread parallel through setupBlockReaders' return signature and into reportReaderStats. process and parallel both carry the roleProcess label and share the same underlying cache, so merge their counters into the same meter set rather than introducing a new "process_parallel" series. See review comment r3191283003. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The diffguard run on commit 4c688e4 flagged 35 surviving mutations (score 77.7%, T1 logic 84.5%). Triage classified seven as HIGH-severity — branch / boolean / conditional mutations on V2's per-tx correctness paths. Add five targeted Tier-1 kill tests for the five that are deterministically killable: * TestPDB_EnableReadTracking_InitializesBalAddrs Pins the `s.BalAddrs == nil` guard at parallel_statedb.go:335. Flipping == to != silently skips the make() on a fresh PDB, leaving cap=0 instead of the documented 8 — every recordBalWrite reallocates. Test asserts cap >= 8 after EnableReadTracking. * TestPDB_PriorDestructedAt_RecordsAbsenceRead Pins the else-if branch at parallel_statedb.go:531. Removing the body drops the absence read recordStoreRead(suicideKey, -1, 0, nil); without it, validation can't catch a concurrent prior tx destructing addr. Test asserts the absence read appears in StoreReads AND that subsequent MVStore writes flip validation to invalid. * TestPDB_Exist_DestructedInBaseReturnsFalse Pins the `if suicideIdx >= 0 { ... }` branch at parallel_statedb.go:576. Removing the body lets a destructed addr fall through to base.Exist and incorrectly return true when the account was set up in base. The test seeds the base StateDB with code on addr (so the fallthrough path is observable) and asserts Exist returns false after a SuicidePath write. * TestPDB_CreateAccount_WritesTrueValue Pins the literal `true` at parallel_statedb.go:1014 (CreateAccount → store.WriteInc). Flipping it to false would publish (CreatePath_addr, txIdx, inc, false), defeating the value-based fallback in storeReadMatches. Test reads the MVStore entry and asserts the value is true. * TestPDB_DiagnoseBalanceRead_MatchReturnsFalse Pins the `false` literal at parallel_statedb_validate.go:215. Flipping to true would have a matching balance read produce a phantom diagnostic with zero-valued fields; DiagnoseValidation would aggregate these as empty "" -category diags. Test asserts len(diags) == 0 on a matching read. The other two HIGH survivors are documented as unkillable in their current form: * parallel_statedb.go:751 GetCodeHash `if len(code) == 0` — removing the early-return falls through to crypto.Keccak256Hash(empty), which equals types.EmptyCodeHash by spec. Behaviourally equivalent; can't be killed without locking in an internal performance signal. * v2_executor.go:586 runValidationLoop `cancelled = true` after ctx-cancel — the mutation's observable effect (drain runs on cancel) is timing-dependent because reexec goroutines exit promptly via ctx.Done() in waitForTx/waitForFinal regardless, so the post-loop drain completes either way. A deterministic kill needs a redesign of the cancel handling. Each new test was verified to: - PASS on unmutated code, - FAIL on the corresponding mutated code (sed/python in-place mutation, run, restore). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- core/blockstm/mvhashmap.go: drop unused MVHashMap.SkipFlush field. The three sibling ablation toggles are wired through the V2 path; SkipFlush alone has no consumer, and as an exported field on a public type it would be SemVer-impactful to remove later. Same dead-scaffolding pattern removed from BlockChain.AblationSkip* in 4c688e4. - core/parallel_state_processor.go: stop discarding the prefetcher-warmed shared JUMPDEST cache. v2Env.Execute previously called evm.SetJumpDestCache(e.jumpDests) unconditionally, overriding the shared cache that vm.NewEVM had just wired from vmConfig. Now the override (and the per-v2Env allocation) are gated on the absence of vmConfig.SharedJumpDestCache: production paths via ProcessBlock keep the prefetcher's analysis; benchmarks and single-block witness paths that bypass that wiring still get the per-v2Env fallback. - core/blockstm/v2_executor.go: wrap the two predecessor receives in v2ExecCtx.execute (completionCh[k-1] and execDone[prev]) with ctx selects, mirroring waitForTx/waitForFinal. Without these, a worker that entered execute past the vfailed[k-1] predicate could hang forever on cancellation — runValidationLoop deliberately skips the finishReexec drain on cancel, so completionCh[k-1] is never closed, wg.Wait blocks indefinitely, and the V2 driver, worker pool, validator/settlement, MVStore/MVBalanceStore/SafeBase, and parallel StateDB all leak. Early return from execute is safe — the worker loop unconditionally closes execDone[taskIdx], unblocking cascading downstream waiters. Benchmarks (V2Embedded, n=10): -2.63% on mean (t=-3.90, statistically significant); V2AllBlocks 241 blocks neutral within noise. Production prefetcher-cache reuse not measurable in current benchmark harnesses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code reviewUnable to complete review — this PR exceeds GitHub's 300-file API diff limit (500+ files, mostly testdata fixtures under Previous review (2026-05-05): No issues found. Checked for bugs and CLAUDE.md compliance. Since that review, 6 follow-up commits were pushed:
These commits address reviewer feedback and fix lint/test issues. A fresh automated review of these changes was not possible due to the access limitations described above. |
|
| go func() { | ||
| pstart := time.Now() | ||
| parallelStatedb.StartPrefetcher("chain", witness, nil) | ||
| res, err := bc.parallelProcessor.Process(block, parallelStatedb, bc.cfg.VmConfig, nil, ctx) | ||
| v2VmCfg := bc.cfg.VmConfig | ||
| sharedCaches.applyTo(&v2VmCfg) | ||
| res, err := bc.parallelProcessor.Process(block, parallelStatedb, v2VmCfg, nil, ctx) | ||
| blockExecutionParallelTimer.UpdateSince(pstart) | ||
| if err == nil { | ||
| vstart := time.Now() | ||
| err = bc.validator.ValidateState(block, parallelStatedb, res, false) | ||
| vtime = time.Since(vstart) | ||
| } | ||
| // If context was cancelled (we lost the race), stop prefetcher | ||
| // before sending result. This prevents "layer stale" errors when | ||
| // the winner's commit advances the pathdb layer. | ||
| if ctx.Err() != nil { | ||
| parallelStatedb.StopPrefetcher() | ||
| } | ||
| if res == nil { | ||
| res = &ProcessResult{} | ||
| } |
There was a problem hiding this comment.
🔴 🟡 V2 prefetcher leaks on the documented V2-failure fallback path. When V2 returns an error (PanickedIdx, ExecErrIdx) and ProcessBlock falls back to serial, the V2 statedb's "v2-settle" prefetcher is never stopped — the ctx.Err() guard at blockchain.go:888-890 cannot fire because the cancel-ordering fix in 4c688e4 deliberately moved cancel() to AFTER the fallback block, and the fallback then reassigns result to V1's value (stopping V1's prefetcher only). The leak is real but only on V2-failure paths (not exercised by the 1M-block soak since V2 wins overwhelmingly); fix is one line — stop the V2 prefetcher unconditionally on the V2 error path before sending the Result, e.g. if err != nil { parallelStatedb.StopPrefetcher() } in the V2 goroutine.
Extended reasoning...
What the bug is
The V2 goroutine in BlockChain.ProcessBlock starts the parallelStatedb prefetcher with StartPrefetcher("chain", witness, nil) at blockchain.go:875. Inside V2StateProcessor.Process, the prefetcher is unconditionally swapped to "v2-settle" at parallel_state_processor.go:1059-1060 (finalDB.StopPrefetcher(); finalDB.StartPrefetcher("v2-settle", prevWitness, nil)). On V2-failure paths — result.PanickedIdx >= 0 (line 1078) or result.ExecErrIdx >= 0 (line 1086) — Process returns nil, err immediately, with no defer cleanup and no explicit StopPrefetcher call. The "v2-settle" prefetcher is left running with live trie references.
Back in the V2 goroutine in blockchain.go, the only cleanup gate is the if ctx.Err() != nil { parallelStatedb.StopPrefetcher() } check at lines 888-890. But the cancel-ordering fix in 4c688e4 deliberately moved cancel() to AFTER the fallback block (now at line 946) — exactly so that V1 can run to completion when V2 fails. The trade-off: at the moment V2 evaluates ctx.Err() on its error path, the context is still active, so the StopPrefetcher call is skipped.
Why the fallback drain doesn't catch it\n\nWhen V2 sends its error result, the main goroutine enters the fallback at blockchain.go:929: result = <-resultChan waits for V1, then result.statedb.StopPrefetcher() at line 935 stops V1's prefetcher (because result was just reassigned to V1's Result). processorCount-- decrements to 1, so the second-drain block at lines 972-975 (if processorCount == 2 { second_result.statedb.StopPrefetcher() }) does NOT fire either. The V2 parallelStatedb reference is now unreachable from the return path, but its subfetcher goroutines (spawned by triePrefetcher.prefetch → go sf.loop() in trie_prefetcher.go) are still alive, blocked on sf.tasks/sf.stop, holding trie reader references.\n\n## Step-by-step proof\n\n1. V2 goroutine starts parallelStatedb.StartPrefetcher("chain", witness, nil) (blockchain.go:875).\n2. V2StateProcessor.Process runs finalDB.StopPrefetcher() then finalDB.StartPrefetcher("v2-settle", prevWitness, nil) (parallel_state_processor.go:1059-1060). The "v2-settle" triePrefetcher spawns subfetcher worker goroutines.\n3. Process hits result.PanickedIdx >= 0 (or ExecErrIdx >= 0) and returns nil, err (parallel_state_processor.go:1078-1080 / 1086-1088). No StopPrefetcher.\n4. V2 goroutine in blockchain.go has err != nil so it skips ValidateState. The guard if ctx.Err() != nil at line 888 evaluates to false (cancel hasn't run yet). StopPrefetcher is not called. V2 sends Result and exits.\n5. Main goroutine receives V2 result. result.parallel && result.err != nil enters the fallback. result = <-resultChan waits for V1. V1 returns successfully (ctx still active). result.statedb.StopPrefetcher() stops V1's prefetcher. processorCount-- = 1.\n6. cancel() finally runs at line 946. V2 goroutine has already exited. cancel() does not propagate into subfetcher.stop (only subfetcher.terminate() closes that channel — see trie_prefetcher.go:399-409).\n7. Final if processorCount == 2 drain at line 972 does not fire.\n8. ProcessBlock returns. The V2 "v2-settle" triePrefetcher goroutines are orphaned, holding live trie reader references.\n\n## Impact\n\n- Goroutine leak per V2-failure block. Each leak persists until process exit.\n- Trie reference leak. The orphaned subfetchers hold references to the parent trie reader, which can collide with the pathdb layer the caller commits afterward. The PR comment at blockchain.go:941-945 explicitly identifies this class of issue ("caller will commit the block (advancing the pathdb layer), which would invalidate any trie references still held by the loser's prefetcher") — but the fallback path leaves V2 in exactly that broken state. Could surface as layer-stale errors on subsequent imports, the very class the cancel-ordering relocation was trying to prevent.\n- Not consensus-affecting (V2's partial state is discarded; serial drives commit).\n- Not exercised by the 1M-block soak since V2 wins overwhelmingly. Manifests only when V2 actually returns an error — which is exactly the documented production fallback contract advertised in the PR description.\n\n## Asymmetry vs V1-failure path\n\nWorth noting: V1-failure does NOT leak because if V1 fails first, processorCount stays at 2 (no decrement), so the final drain at line 972 fires and stops V2's prefetcher. Only V2-failure decrements processorCount to 1 inside the fallback block, bypassing the drain.\n\n## How to fix\n\nMinimal one-line fix in the V2 goroutine in blockchain.go, after the Process+ValidateState block, before sending the Result:\n\ngo\nif err != nil {\n parallelStatedb.StopPrefetcher()\n}\n\n\nThis mirrors the symmetry that the existing if ctx.Err() != nil guard provides for the cancellation case. Alternative locations that work equally well: a defer parallelStatedb.StopPrefetcher() inside V2StateProcessor.Process after the prefetcher swap, or capturing+stopping the V2 prefetcher in the fallback block before the result = <-resultChan reassignment overwrites the reference.
| func (s *MVStore) shard(k Key) *mvStoreShard { | ||
| h := uint(k[0])<<8 | uint(k[1]) | ||
| return &s.shards[h%mvStoreShards] | ||
| } |
There was a problem hiding this comment.
🟡 Sharded-store shard hash collapses k[0] entropy via 256-mod-power-of-2. All three sharded concurrent stores compute their shard via h := uint(k[0])<<8 | uint(k[1]); h % shards (mvstore.go:59-62 with mvStoreShards=64, mvbalance_store.go:51-54 with mvBalanceShards=64, mvhashmap.go:186-191 with numShards=16). Because 256 % 64 == 0 and 256 % 16 == 0, the modulo collapses to k[1] % shards — k[0] contributes zero entropy. Polygon's heaviest hot keys are system contracts (StateReceiver 0x…001000, ValidatorSet 0x…001001, MATIC 0x…001010) which all share k[0]=k[1]=0x00 and therefore all hit shard 0 of every sharded store, serializing concurrent writes through one RWMutex while the other 63 (or 15) shards sit idle. Pure performance — no correctness impact. One-line fix: mix in trailing address bytes where the entropy lives (e.g. h := uint(k[0])^uint(k[18])<<8 | uint(k[1])^uint(k[19])) so address-only and subpath system-contract keys actually distribute across shards.
Extended reasoning...
What the bug is
All three sharded concurrent stores added by V2 use the same shard-hash formula:
h := uint(k[0])<<8 | uint(k[1])
return &s.shards[h%mvStoreShards] // mvStoreShards = 64(mvstore.go:60, mvbalance_store.go:52, mvhashmap.go:189 — last one with numShards=16.)
Math: h = k[0]*256 + k[1]. For any modulus m that divides 256, we have k[0]*256 ≡ 0 (mod m), so h % m ≡ k[1] % m. 64 divides 256 (256 = 4·64) and 16 divides 256 (256 = 16·16). k[0] contributes exactly zero entropy to shard selection.
Sanity check, all give shard 0 for k[1]=0x00:
- (0x00<<8 | 0x00) % 64 = 0
- (0xFF<<8 | 0x00) % 64 = 0
- (0xAB<<8 | 0x00) % 64 = 0
How it manifests on Polygon
Polygon system contracts live at low-numbered addresses where k[0]=k[1]=0x00:
0x0000000000000000000000000000000000001000— Validator Set0x0000000000000000000000000000000000001001— State-Sync Receiver0x0000000000000000000000000000000000001010— MATIC ERC20
All three system contracts hash to shard 0 in all three sharded stores. State-sync runs every sprint-end block (every 16 blocks on Bor) and writes to the State Receiver. MATIC transfers are pervasive across DeFi. So shard 0 sees materially more activity than the other 63 (or 15) shards.
Within a single block's parallel run with N workers, every worker that touches a system-contract key serializes through shard 0's RWMutex. Concurrent reads coexist (RLock), but WriteInc / WriteDelta / MarkEstimate / CleanupEstimate take the write lock and hold it through the slice search + modify.
Step-by-step proof
- Tx A in some block performs
balanceStore.WriteDelta(0x...001010, ...)— MATIC transfer.shard()computesh = 0x00<<8 | 0x00 = 0,h % 64 = 0. Lands onshards[0]. - Tx B (concurrent) performs
mvStore.WriteInc(NewSubpathKey(0x...001000, NoncePath), ...)— state-sync receiver write.shard()again computes 0. Same shard. - Tx C (concurrent) reads MATIC balance via
balanceStore.ReadDelta(0x...001010, ...). Hits shard 0's RLock. - Tx D performs
mvHashMap.Write(NewStateKey(0x...001001, slot), ...)— validator set storage write. Hits shard 0's Lock.
Every write among txs A, B, D contends on shards[0].mu while shards[1..63] (and shards[1..15] in the MVHashMap case) sit idle.
Why existing safeguards do not catch this
- The Go race detector cannot flag this — the locking is correct, just suboptimally distributed.
- The 1M-block production soak achieves 1.6× throughput speedup despite this hot-spot, so it does not surface as a correctness or stability issue.
- No test asserts uniform shard distribution.
- The author's bloom-filter fix earlier in this PR (commit 4c688e4) addressed an analogous "low-byte entropy" issue (h2 reading k[20:24] which is zero for address-only and subpath keys); this is the same shape of bug in a different file.
Impact
Pure performance regression on Polygon's documented hot-key cluster. No correctness, security, consensus, or fund-safety impact — sequential fallback parity is unaffected since shard distribution doesn't change determinism. Fix is mechanical and not pre-merge blocking, but it's worth flagging because (a) the modulo-collapse math is unambiguous, (b) the affected workload (state-sync sprint-end blocks, system-contract writes) is on the documented hot path, and (c) the same class of bug was already accepted earlier in this PR review for the bloom filter.
How to fix
The proposed fix in earlier verifier commentary (h := uint(k[0])^uint(k[1])<<3 ^ uint(k[2])^uint(k[3])<<3) is incomplete for Polygon's specific case — system contracts have k[0..3] all 0x00, so XOR-folding the leading bytes still gives shard 0. The cleanest fix mixes in trailing address bytes where Polygon's address entropy actually lives (bytes 18-19 distinguish 0x...1000 / 0x...1001 / 0x...1010):
func (s *MVStore) shard(k Key) *mvStoreShard {
h := uint(k[0])^uint(k[18])<<8 | uint(k[1])^uint(k[19])
return &s.shards[h%mvStoreShards]
}Apply identically to MVBalanceStore.shard (addr[18], addr[19]) and MVHashMap.getShard. With this fix, 0x...001000 → shard derived from (0x00^0x10)<<8 | (0x00^0x00) = 0x1000 = 4096; 4096 % 64 = 0 — hmm, still 0 for that one. A safer choice is to feed the address tail through a small mixing step, e.g. fnv1a32(k[16:20]) or even just (uint(k[18])*131) ^ uint(k[19]) — anything non-power-of-2-aligned breaks the collapse.
🔬 also observed by verify-runtime



BlockSTM v2
Introduces BlockSTM v2 — a from-scratch redesign of Bor's parallel
transaction execution engine. V2 speculatively executes block transactions
concurrently, validates each tx's reads against a multi-version store, and
re-executes any whose reads turned stale. On the 241-block mainnet witness
benchmark V2/4w delivers ~1.6× throughput over serial (570 mgas/s vs
350 mgas/s, AMD Ryzen 7 5800H, all-in-memory).
Architecture
V2 runs three coordinated goroutine groups around a per-tx PDB:
Backed by:
SafeBase— thread-safe base reads (sync.Mapcaches over a boundedpool of
StateDB.Copy()with concurrent-reads mode on the trieReader).MVStore— sharded multi-version per-key store with a lock-free bloomfilter for cold-key reads.
MVBalanceStore— sharded commutative balance delta store (per-txAdd/Sub; reads sum prior entries).
Execution flow
V2Tasks. Same-senderchains get pre-computed nonces (
SenderNonces) so nonce reads on achain are skipped during validation.
dispatcher (window
numWorkers * InFlightTaskMultiplier). Each tx runsin its own
ParallelStateDB; reads come fromSafeBase+MVStore+MVBalanceStoreand are recorded inStoreReads/BalReads. Writesaccumulate locally (
DeferMVWrites) and flush toMVStoreat end-of-txso concurrent readers only ever see FINAL values — never mid-tx
reentrancy-guard writes.
order. Each recorded read is re-checked against
MVStore; match bywriter/incarnation OR by value-equal fallback (handles idempotent writes
such as reentrancy-guard SSTOREs that flip back). Mismatch →
MarkEstimatethe failed tx's writes and dispatch a re-executiongoroutine. Per-key pipelining: readers that hit an
ESTIMATEentryunder
Incarnation > 0block onWaitForFinaluntil the upstreamwriter is finalized.
chSettlein tx-index order and applies each tx's writes tofinalDB(the real, single-threaded
*state.StateDB) through a*Directsetterfamily that bypasses the journal, then asks
finalDBfor theIntermediateRoot.Testing
V2 is gated on a layered test surface. From cheapest to most expensive,
and what each layer is meant to catch:
1. Compile-time conformance + drift detection
The PDB shadows
StateDB's interface and behaviour, so any upstreamgo-ethereum merge that adds or changes a
StateDBmethod would silentlybypass V2. A handful of
go test-time checks fail CI before any logicruns:
core/vm/statedb_impl_test.govm.StateDBvia a static assertionTestPDBMethodParityStateDBmethod has a PDB mapping or is inpdbExemptMethodsTestV2DependencyCompileCheckStateDBmethod V2 settle calls remains presentTestV2JournalEntryCoverageparallelJournalEntrymappingTestV2TracingHookParitytracing.Hooksfield is classified as fired-or-skippedTestV2ForkParityparams.ChainConfig.IsXfork rule is classified V1/V22. Per-method unit tests (~210 tests across ~25 files)
Cover individual PDB getters/setters,
MVStore/MVBalanceStoreprimitives, V2 executor channel mesh, and
SettleTohelpers. Highlights:core/state/parallel_statedb_test.gocore/state/parallel_statedb_coverage_test.gocore/state/parallel_statedb_getter_table_test.goWriterIdxacross Committed / ESTIMATE / NoEntry / AtTxZerocore/state/safe_base_test.gosync.Mapcache + pool semanticscore/blockstm/mvstore_test.go,core/blockstm/mvbalance_store_test.gocore/blockstm/v2_executor_wait_test.gowaitForTx/waitForFinal+ cancellation3. Direct-setter parity tests
The
*Directsetter family bypassesStateDB's journal at settle time.core/state/v2_direct_setter_parity_test.go(7 tests) pins thatSetXDirectproduces a byte-identical state root to journaledSetX + Finalise. Catches divergence the moment a future change to eitherpath breaks the parity.
4. Differential tests vs serial StateDB
Hand-written + table-driven scenarios that exercise the PDB against a
parallel-mirror serial
StateDBand assert byte-identical output. Catchesbehaviour drift the parity-table tests can't express:
core/state/v2_differential_test.go— PDB-only diffcore/state/v2_executor_differential_test.go— synthetic-env executor diffcore/v1_differential_test.go— V1 vs serial parity for the legacyin-tree path
5. Mutation testing (Tier-1 kill tests)
diffguardruns mutation testing against V2's critical paths. Everysurvivor flagged by a sample run has a corresponding targeted test inline
in
core/state/parallel_statedb_test.gounder the "Tier-1 mutation killtests" divider — boundary, negation, and return-value mutations on
storeReadMatches, journal revert,settleTohelpers,applyFeeData,Reset, etc. Tier-1 logic kill-rate ≥99% on the latest run.6. Fuzz targets
Randomized inputs against either a serial mirror or a hand-built reference:
core/state/v2_fuzz_test.go— random PDB op sequences vsStateDBcore/state/v2_executor_fuzz_test.go— executor-level fuzz onsynthetic env
core/v2_serial_parity_fuzz_test.go(FuzzV2ExecutorVsSerial) — randomtx batches through
ExecuteV2BlockSTMvs anApplyMessageloopThe race-detected fuzz under
-racecaught the shared-trie-reader racethat the non-race fuzz missed; worth keeping on the nightly.
7. End-to-end consistency + benchmark on real mainnet blocks
core/mainnet_witness_benchmark_test.gobundles 241 real Polygon mainnetblocks (under
core/blockstm/testdata/) with their pre-block witnesses.Two harnesses share the corpus:
TestV2BlockSTMAllBlocks(gated onBOR_BLOCKSTM_TEST=1) replays eachblock through both serial and V2 and asserts byte-identical state roots
and receipt roots.
BenchmarkV2AllBlocksruns serial + V2 across worker counts (4 / 8 / 16)and witness-on/off variants on the same corpus. Backs the throughput
numbers referenced at the top of this commit.
8. Runtime invariants under
-tags=invariantsBuild-tag-gated runtime assertions inside the executor and the PDB. Off in
production builds (zero-cost), on in CI:
assertSettleOrder— validation walk's inductionassertReexecVisitedExactlyOnce— drain loop doesn't lose a txassertSettleNotPanicked— panicked PDBs never settleA tiny set of "panic if invariant breaks" tests under
//go:build invariantsverifies the assertions actually fire on craftedviolations (
core/blockstm/v2_executor_invariants_panic_test.go,core/state/parallel_statedb_invariants_panic_test.go).9. Race detector
All of layers 2–8 are runnable under
go test -race. CI runs the fullstate + blockstm packages in race mode; the
TestV2BlockSTMAllBlocksgated test is also race-clean on the 241-block corpus.
10. Production soak — >1 million Polygon mainnet blocks
Beyond the unit / parity / fuzz layers above, this branch has been used to
sync more than 1,000,000 mainnet blocks end-to-end on a real node with V2
as the primary processor (with serial disabled). Zero state-root
divergences, zero panics requiring fallback, no consensus-affecting issues
observed. This is the most stringent layer: real on-chain workload, real
database backend, real prefetcher contention.
Bundled additions
intermediateRootTimermetric (chain/intermediateroot) — measures thepost-execution trie computation in
block_validator.go.Major changes
The code surface is ~5.1k lines across 39 production
.gofiles, plus~11.7k lines across 37 test files. The remaining 484 file entries in the
diff are block + witness fixtures under
core/blockstm/testdataused byTestV2BlockSTMAllBlocksand the benchmark harness — read-only data, noreview needed.
Shapes of change a reviewer should expect:
ParallelStateDBshadows*state.StateDBbutreads from
SafeBase+MVStore+MVBalanceStoreand tracks reads forvalidation. Implements
vm.StateDB. Has its own journal layer(
parallelJournalEntry) parallel toStateDB'sjournal.go.MVStore(sharded multi-version per-key storewith bloom filter) and
MVBalanceStore(sharded commutative balancedeltas) — both new, both load-bearing.
ExecuteV2BlockSTMowns the worker pool + in-ordervalidator + pipelined settle goroutine and the
chSettle/completionCh/execDonechannel mesh between them.SafeBaseis a thread-safe wrapperaround a
*state.StateDBwithsync.Mapcaches + a bounded pool ofdb.Copy()instances; the pool copies share the underlying reader, sothe V2 entry point flips trieReader into its concurrent-reads mode
(
sync.Mapnode-resolve cache instead of in-place mutation) — thisrequired surgery in
state/database.go,state/reader.go,state/trie_prefetcher.go,trie/trie.go,trie/secure_trie.go,triedb/pathdb/reader.go, andtriedb/pathdb/biased_fastcache.go.*Directsetter family onStateDB. Bypasses the journal at settletime so V2 can replay per-tx PDB writes onto
finalDBdeterministically. Pinned byte-equal to journaled
SetX + FinalisebyTestDirectSetterParity_*.BlockChainwires V2 as the primary processorand falls back to serial on panics,
ApplyMessageconsensus errors,ctxcancellation, and witness requests.Files that warrant the most reviewer attention
Tier 1 — load-bearing executor + per-tx state
core/blockstm/v2_executor.gocore/parallel_state_processor.goV2StateProcessor, settle-fn closure, envcore/state/parallel_statedb.gocore/state/parallel_statedb_validate.gocore/state/parallel_statedb_settle.gocore/state/parallel_statedb_journal.gocore/state/safe_base.goTier 2 — concurrent stores
core/blockstm/mvstore.gocore/blockstm/mvbalance_store.goTier 3 — modified upstream files (highest merge-conflict risk)
core/state/statedb.go*Directsetters,skipTimers, concurrent-reads enablercore/state/state_object.gocore/state/database.gocore/state/reader.gocore/state/trie_prefetcher.gotrie/trie.go,trie/secure_trie.gotriedb/pathdb/reader.gosync.Mapnode-resolve cache for concurrent reads; small lock changestriedb/pathdb/biased_fastcache.gocore/vm/evm.go,jumpdests.go,instructions.go,interface.go,interpreter.goStateDBiface addscore/blockchain.gocore/state_transition.goTier 4 — drift-detection tests
Read these to understand the parity contract V2 must hold against
StateDB:core/state/v2_method_parity_test.goStateDBmethod has a PDB mappingcore/state/v2_journal_entry_coverage_test.gocore/state/v2_direct_setter_parity_test.goSetXDirect↔ journaledcore/state/parallel_statedb_getter_table_test.gocore/parallel_state_processor_hooks_parity_test.gotracing.Hooksfire-or-skip classificationcore/parallel_state_processor_fork_parity_test.goparams.IsXclassificationcore/v2_serial_parity_fuzz_test.gocore/mainnet_witness_benchmark_test.goSee
docs/blockstm-v2.mdfor full architecturaldetail, the list of correctness bug classes V2 prevents, and
ongoing-improvement notes.